package org.dingyom.car.service.impl;

import org.dingyom.basics.constants.BaseConstants;
import org.dingyom.car.domain.CarType;
import org.dingyom.car.mapper.CarTypeMapper;
import org.dingyom.car.service.ICarTypeService;
import org.dingyom.basics.service.impl.BaseServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author dingyom
 * @since 2022-09-05
 */
@Service
public class CarTypeServiceImpl extends BaseServiceImpl<CarType> implements ICarTypeService {

    @Autowired
    private CarTypeMapper carTypeMapper;
    @Autowired
    private RedisTemplate redisTemplate;
    /*增删改的时候要更新缓存
    * 方法1 增删改的时候 把缓存中的数据从新插入一次 没什么人用
    * 方法2 增删改的时候 把缓存删除 操作简单 效率也高（采用）*/
    //==============================自己实现缓存================================//
    /*比用cache更灵活 处理缓存问题
    * 缓存穿透 数据既不在缓存中，也不在数据库中 这样每次访问都会访问到数据库 相当于缓存被穿透了
    * 缓存击穿 某个缓存过期了，大量请求访问这个数据,就会访问数据库  解决方式：互斥锁方案（采用），不给热点数据设置过期时间
    * 缓存雪崩 多个缓存同时过期或者redis宕机，请求访问这些缓存 访问到数据库  解决方案：双key策略
    * */
    @Override
    public void add(CarType carType) {
        super.add(carType);
        //更新主key 备key
        List<CarType> carTypes = getCarTypesByLoop(0L);
        setCacheData(BaseConstants.CARTYPE_TREE_CONSTANT,carTypes);
    }

    @Override
    public void modify(CarType carType) {
        super.modify(carType);
        //更新主key 备key
        List<CarType> carTypes = getCarTypesByLoop(0L);
        setCacheData(BaseConstants.CARTYPE_TREE_CONSTANT,carTypes);
    }

    @Override
    public void delete(Serializable id) {
        super.delete(id);
        //更新主key 备key
        List<CarType> carTypes = getCarTypesByLoop(0L);
        setCacheData(BaseConstants.CARTYPE_TREE_CONSTANT,carTypes);
    }

    //缓存穿透和缓存击穿 解决方案
    /*@Override
    public List<CarType> tree(long pid) {
        //查询数据库之前先去缓存中查是否存在
        Object obj = redisTemplate.opsForValue().get(BaseConstants.CARTYPE_TREE_CONSTANT);

        if (obj!=null){//缓存中有数据 直接返回
            System.out.println("缓存中有数据");
            return (List<CarType>) obj;
        }else {//缓存中没有 才去数据库中查询
            //上锁 一次只能一个访问，访问到数据后更新进缓存中 增加双重判断 其他请求就可以在缓存中拿
            synchronized (CarTypeServiceImpl.class){//互斥锁方案
                Object objTem = redisTemplate.opsForValue().get(BaseConstants.CARTYPE_TREE_CONSTANT);
                if (objTem != null) {//缓存中有该数据了
                    return (List<CarType>) objTem;
                }
                System.out.println("缓存中没有数据");
                List<CarType> carTypes = getCarTypesByLoop(pid);
                //就算没有数据是null 也要存进缓存 这样就不会被穿透
                redisTemplate.opsForValue().set(BaseConstants.CARTYPE_TREE_CONSTANT, carTypes);
                return carTypes;
            }
        }
    }*/

    //双key策略 备用key永不过期
    @Override
    public List<CarType> tree(long pid) {
        //查询数据库之前先去缓存中查是否存在
        Object obj = getCacheData(BaseConstants.CARTYPE_TREE_CONSTANT);
        if (obj!=null){//缓存中有数据 直接返回
            System.out.println("缓存中有数据");
            return (List<CarType>) obj;
        }else {//缓存中没有 才去数据库中查询
            //上锁 一次只能一个访问，访问到数据后更新进缓存中 增加双重判断 其他请求就可以在缓存中拿
            synchronized (CarTypeServiceImpl.class){//互斥锁方案
                Object objTem = getCacheData(BaseConstants.CARTYPE_TREE_CONSTANT);
                if (objTem != null) {//缓存中有该数据了
                    return (List<CarType>) objTem;
                }
                System.out.println("缓存中没有数据");
                List<CarType> carTypes = getCarTypesByLoop(pid);
                //就算没有数据是null 也要存进缓存 这样就不会被穿透
                setCacheData(BaseConstants.CARTYPE_TREE_CONSTANT, carTypes);//更新主key和备key
                return carTypes;
            }
        }
    }

    /**
     * 双key策略查询
     * 先查询主key 如果没有再查询备key
     * @param key
     * @return
     */
    private Object getCacheData(String key){
        //查询主key
        Object obj = redisTemplate.opsForValue().get(key);
        if (obj!=null)//缓存中有数据 直接返回
            return obj;
        //查询备key
        obj = redisTemplate.opsForValue().get(key + "_bak");
        if (obj!=null){//备用key不为空 就说明之前是存在的 需要查询数据库 更新主key和备key数据
            //就算没有数据是null 也要存进缓存 这样就不会被穿透
            setCacheData(key, obj);//更新主key和备key
            return obj;
        }
        //如果主key和备key都没有 那就返回null
        return null;
    }

    /**
     * 更新主key和备用key
     * @param key
     * @param obj
     */
    private void setCacheData(String key, Object obj){
        redisTemplate.opsForValue().set(key, obj);//更新主key
        redisTemplate.opsForValue().set(key+"_bak", obj);//更新备key
    }
    //==============================自己实现缓存================================//
    /**
     * 循环查询树
     * @param pid
     * @return
     */
    private List<CarType> getCarTypesByLoop(Long pid) {
        //声明返回集合
        List<CarType> result = new ArrayList<>();
        //查询所有
        List<CarType> carTypes = carTypeMapper.loadAll();
        //为了方便获取父亲建立的id和类型的映射关系 用一个map来存放数据
        Map<Long,CarType> map = new HashMap<>();
        for (CarType carType : carTypes) {//N次循环
            map.put(carType.getId(),carType);
        }
        //遍历查询出来的carTypes 插入
        carTypes.forEach(carType -> {
            Long pidTmp = carType.getPid();
            if(pidTmp.longValue()==pid.longValue()){//就是顶级父类型 直接放入放回集合
                result.add(carType);
            }else {//不是顶级类型 就需要放到子集类型中
                //获取父级类型
                /*方法1 再遍历一次carTypes 父级类型也在里面
                 * 缺点 时间复杂度 N*N 100*100=10000 效率低*/
                /*CarType parent = null;
                for (CarType type : carTypes) {
                    if(type.getId()==carType.getPid()){//id==pid就说明是父级类型
                        //这就是父级类型
                        parent = type;
                        break;
                    }
                }*/
                /*方法2 通过map 的key直接获取父级类型
                 * 时间复杂度 就只有N 空间换时间*/
                CarType parent = map.get(pidTmp);
                //当做父亲儿子
                parent.getChildren().add(carType);//把自己放入父亲的集合
            }
        });
        return result;
    }

    /**
     * 递归查询树
     * @param pid
     * @return
     */
    private List<CarType> getCarTypesByRecursion(long pid) {
        List<CarType> carTypes = carTypeMapper.loadByPid(pid);
        if (carTypes == null) {//表示没有以当前父级id为父级类型的子集
            return null;
        }else {
            //将自己的id 作为父级id 递归调自己 返回的值保存进子集类型
            carTypes.forEach(carType -> {
                carType.setChildren(this.tree(carType.getId()));
            });
        }
        return carTypes;
    }
}
